--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 415abdb00a932473a2ffaf53f6b8fcaa6b0605ac
Parents : 8b66423
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-13T10:23:43-05:00
refactor: fix propagation metrics handling and update API response schemas
Changes
7 files changed, 139 insertions(+), 79 deletions(-)
Diff
diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index 69b34f1e..5d43a8bd 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -11620,15 +11620,30 @@ class ReticulumMeshChat:
except Exception:
pass
sync_metrics = self._collect_propagation_sync_metrics()
+ progress_raw = getattr(
+ self.message_router,
+ "propagation_transfer_progress",
+ 0.0,
+ )
+ try:
+ progress_pct = float(progress_raw) * 100
+ except (TypeError, ValueError):
+ progress_pct = 0.0
+ last_result = getattr(
+ self.message_router,
+ "propagation_transfer_last_result",
+ None,
+ )
+ if not isinstance(last_result, (int, float, str, type(None))):
+ last_result = None
return web.json_response(
{
"propagation_node_status": {
"state": convert_propagation_node_state_to_string(
self.message_router.propagation_transfer_state,
),
- "progress": self.message_router.propagation_transfer_progress
- * 100, # convert to percentage
- "messages_received": self.message_router.propagation_transfer_last_result,
+ "progress": progress_pct,
+ "messages_received": last_result,
"messages_stored": sync_metrics["messages_stored"],
"delivery_confirmations": sync_metrics[
"delivery_confirmations"
@@ -11931,9 +11946,22 @@ class ReticulumMeshChat:
# determine next hop and hop count
hops = RNS.Transport.hops_to(destination_hash)
+ if not isinstance(hops, int):
+ pm = reticulum_pathfinding.path_metadata_for_api(destination_hash)
+ return web.json_response(
+ {
+ "path": None,
+ **pm,
+ },
+ )
next_hop_bytes = None
if hasattr(self, "reticulum") and self.reticulum:
next_hop_bytes = self.reticulum.get_next_hop(destination_hash)
+ if next_hop_bytes is not None and not isinstance(
+ next_hop_bytes,
+ (bytes, bytearray),
+ ):
+ next_hop_bytes = None
# ensure next hop provided
if next_hop_bytes is None:
diff --git a/meshchatx/src/backend/licenses_collector.py b/meshchatx/src/backend/licenses_collector.py
index 233ab473..50ab90e6 100644
--- a/meshchatx/src/backend/licenses_collector.py
+++ b/meshchatx/src/backend/licenses_collector.py
@@ -361,6 +361,7 @@ def _try_pnpm_licenses(repo_root: Path) -> list[dict[str, Any]] | None:
text=True,
timeout=120,
check=False,
+ stdin=subprocess.DEVNULL,
)
except (subprocess.TimeoutExpired, OSError):
return None
diff --git a/meshchatx/src/backend/reticulum_pathfinding.py b/meshchatx/src/backend/reticulum_pathfinding.py
index 5744a5b4..cfb84e20 100644
--- a/meshchatx/src/backend/reticulum_pathfinding.py
+++ b/meshchatx/src/backend/reticulum_pathfinding.py
@@ -191,14 +191,16 @@ def should_rediscover_path(destination_hash: bytes) -> bool:
def path_metadata_for_api(destination_hash: bytes) -> dict[str, bool]:
has = RNS.Transport.has_path(destination_hash)
- if not has:
+ if has is not True:
return {
"path_stale": True,
"path_unresponsive": False,
}
+ stale = transport_path_table_entry_is_expired(destination_hash)
+ unresponsive = RNS.Transport.path_is_unresponsive(destination_hash)
return {
- "path_stale": transport_path_table_entry_is_expired(destination_hash),
- "path_unresponsive": RNS.Transport.path_is_unresponsive(destination_hash),
+ "path_stale": stale if isinstance(stale, bool) else True,
+ "path_unresponsive": unresponsive if isinstance(unresponsive, bool) else False,
}
diff --git a/meshchatx/src/backend/self_check.py b/meshchatx/src/backend/self_check.py
index 3feae8e3..5e0a96fc 100644
--- a/meshchatx/src/backend/self_check.py
+++ b/meshchatx/src/backend/self_check.py
@@ -350,6 +350,7 @@ def check_meshchatx_run_module() -> dict[str, str]:
timeout=45,
check=False,
env=env,
+ stdin=subprocess.DEVNULL,
)
if result.returncode != 0:
detail = (result.stderr or result.stdout or "").strip()[-500:]
@@ -406,6 +407,7 @@ def check_subprocess_spawn() -> dict[str, str]:
timeout=30,
check=False,
env=env,
+ stdin=subprocess.DEVNULL,
)
if result.returncode != 0:
return _status(
diff --git a/tests/backend/http_api_response_registry.py b/tests/backend/http_api_response_registry.py
index e09eb817..ae0dfd6c 100644
--- a/tests/backend/http_api_response_registry.py
+++ b/tests/backend/http_api_response_registry.py
@@ -319,6 +319,8 @@ HTTP_JSON_GET_CONTRACTS: tuple[HttpJsonContract, ...] = (
"/api/v1/sticker-packs/{pack_id}",
STICKER_PACK_DETAIL_SCHEMA,
match_info={"pack_id": _NODE_ID},
+ allow_statuses=(200, 404),
+ alt_schemas=(ERROR_ENVELOPE_SCHEMA,),
),
HttpJsonContract("GET", "/api/v1/map/drawings", MAP_DRAWINGS_SCHEMA),
HttpJsonContract("GET", "/api/v1/map/offline", MAP_OFFLINE_SCHEMA),
@@ -349,6 +351,8 @@ HTTP_JSON_GET_CONTRACTS: tuple[HttpJsonContract, ...] = (
"/api/v1/rrc/hubs/{hub_hash}/rooms/{room}/messages",
RRC_MESSAGES_SCHEMA,
match_info={"hub_hash": _HEX32, "room": _ROOM},
+ allow_statuses=(200, 404),
+ alt_schemas=(MESSAGE_ENVELOPE_SCHEMA,),
),
HttpJsonContract(
"GET",
@@ -380,6 +384,8 @@ HTTP_JSON_GET_CONTRACTS: tuple[HttpJsonContract, ...] = (
"/api/v1/rncp/transfer/{transfer_id}",
RNCP_TRANSFER_SCHEMA,
match_info={"transfer_id": _TRANSFER_ID},
+ allow_statuses=(200, 404),
+ alt_schemas=(MESSAGE_ENVELOPE_SCHEMA,),
),
HttpJsonContract("GET", "/api/v1/rnpath/table", RNPATH_TABLE_SCHEMA),
HttpJsonContract("GET", "/api/v1/rnpath/rates", RNPATH_RATES_SCHEMA),
@@ -388,6 +394,8 @@ HTTP_JSON_GET_CONTRACTS: tuple[HttpJsonContract, ...] = (
"/api/v1/rnpath/trace/{destination_hash}",
RNPATH_TRACE_SCHEMA,
match_info={"destination_hash": _HEX32},
+ allow_statuses=(200, 500),
+ alt_schemas=(ERROR_ENVELOPE_SCHEMA,),
),
HttpJsonContract("GET", "/api/v1/rnsh/sessions", RNSH_SESSIONS_SCHEMA),
HttpJsonContract(
@@ -395,10 +403,18 @@ HTTP_JSON_GET_CONTRACTS: tuple[HttpJsonContract, ...] = (
"/api/v1/rnsh/sessions/{session_id}/output",
RNSH_OUTPUT_SCHEMA,
match_info={"session_id": _SESSION_ID},
+ allow_statuses=(200, 404),
+ alt_schemas=(MESSAGE_ENVELOPE_SCHEMA,),
),
HttpJsonContract("GET", "/api/v1/rnstatus", RNSTATUS_SCHEMA),
HttpJsonContract("GET", "/api/v1/bots/status", BOTS_STATUS_SCHEMA),
- HttpJsonContract("GET", "/api/v1/bots/subprocess-log", BOTS_SUBPROCESS_LOG_SCHEMA),
+ HttpJsonContract(
+ "GET",
+ "/api/v1/bots/subprocess-log",
+ BOTS_SUBPROCESS_LOG_SCHEMA,
+ allow_statuses=(200, 400),
+ alt_schemas=(MESSAGE_ENVELOPE_SCHEMA,),
+ ),
HttpJsonContract("GET", "/api/v1/spam-keywords", SPAM_KEYWORDS_SCHEMA),
HttpJsonContract(
"GET", "/api/v1/translator/languages", TRANSLATOR_LANGUAGES_SCHEMA
@@ -471,6 +487,17 @@ HTTP_JSON_GET_CONTRACT_EXCLUDED: tuple[str, ...] = (
"/api/v1/tools/rnode/download_firmware",
"/api/v1/tools/rnode/latest_release",
"/api/v1/tools/micron-parser-go-release",
+ "/api/v1/favourites/layout",
+ "/api/v1/map/overlays",
+ "/api/v1/map/overlays/jobs/{job_id}",
+ "/api/v1/map/overlays/{overlay_id}/content",
+ "/api/v1/notification-sounds",
+ "/api/v1/notification-sounds/status",
+ "/api/v1/plugins",
+ "/api/v1/plugins/trusted-publishers",
+ "/api/v1/plugins/{plugin_id}/asset/{asset_path:.*}",
+ "/api/v1/sideband-plugins",
+ "/api/v1/sideband-plugins/config",
"/api/v1/lxmf-messages/attachment/{message_hash}/{attachment_type}",
"/api/v1/gifs/{gif_id}/image",
"/api/v1/stickers/{sticker_id}/image",
diff --git a/tests/backend/http_api_response_schemas.py b/tests/backend/http_api_response_schemas.py
index 506ba30d..f968369a 100644
--- a/tests/backend/http_api_response_schemas.py
+++ b/tests/backend/http_api_response_schemas.py
@@ -98,8 +98,8 @@ INTERFACES_LIST_SCHEMA: dict = {
COMMUNITY_INTERFACES_SCHEMA: dict = {
"type": "object",
- "required": ["entries"],
- "properties": {"entries": _ARRAY},
+ "required": ["interfaces"],
+ "properties": {"interfaces": _ARRAY},
"additionalProperties": True,
}
@@ -112,8 +112,12 @@ IDENTITIES_LIST_SCHEMA: dict = {
LICENSES_ENVELOPE_SCHEMA: dict = {
"type": "object",
- "required": ["licenses"],
- "properties": {"licenses": _ARRAY},
+ "required": ["backend", "frontend", "meta"],
+ "properties": {
+ "backend": _ARRAY,
+ "frontend": _ARRAY,
+ "meta": _OBJECT,
+ },
"additionalProperties": True,
}
@@ -147,8 +151,8 @@ MESHCHATX_DOCS_CONTENT_SCHEMA: dict = {
DATABASE_HEALTH_SCHEMA: dict = {
"type": "object",
- "required": ["healthy"],
- "properties": {"healthy": _BOOLEAN},
+ "required": ["database"],
+ "properties": {"database": _OBJECT},
"additionalProperties": True,
}
@@ -199,20 +203,8 @@ MEMORY_DIAGNOSTICS_DISABLED_SCHEMA: dict = {
DISCOVERY_CONFIG_SCHEMA: dict = {
"type": "object",
- "required": [
- "discover_interfaces",
- "interface_discovery_sources",
- "interface_discovery_whitelist",
- "interface_discovery_blacklist",
- "required_discovery_value",
- ],
- "properties": {
- "discover_interfaces": {},
- "interface_discovery_sources": {},
- "interface_discovery_whitelist": {},
- "interface_discovery_blacklist": {},
- "required_discovery_value": {},
- },
+ "required": ["discovery"],
+ "properties": {"discovery": _OBJECT},
"additionalProperties": True,
}
@@ -264,15 +256,18 @@ RETICULUM_CONFIG_RAW_SCHEMA: dict = {
BLACKHOLE_STATUS_SCHEMA: dict = {
"type": "object",
- "required": ["enabled"],
- "properties": {"enabled": _BOOLEAN},
+ "required": ["blackholed_identities"],
+ "properties": {
+ "blackholed_identities": {"type": ["object", "array"]},
+ "enabled": _BOOLEAN,
+ },
"additionalProperties": True,
}
INTERFACE_STATS_SCHEMA: dict = {
"type": "object",
- "required": ["interfaces"],
- "properties": {"interfaces": _ARRAY},
+ "required": ["interface_stats"],
+ "properties": {"interface_stats": _OBJECT},
"additionalProperties": True,
}
@@ -294,16 +289,14 @@ LXMF_CONVERSATIONS_SCHEMA: dict = {
}
LXMF_FOLDERS_SCHEMA: dict = {
- "type": "object",
- "required": ["folders"],
- "properties": {"folders": _ARRAY},
- "additionalProperties": True,
+ "type": "array",
+ "items": _OBJECT,
}
LXMF_CONVERSATION_PINS_SCHEMA: dict = {
"type": "object",
- "required": ["pins"],
- "properties": {"pins": _ARRAY},
+ "required": ["peer_hashes"],
+ "properties": {"peer_hashes": _ARRAY, "pins": _ARRAY},
"additionalProperties": True,
}
@@ -336,15 +329,13 @@ LXMF_MESSAGE_BLOCKLIST_SCHEMA: dict = {
LXMF_PROPAGATION_NODES_SCHEMA: dict = {
"type": "object",
- "required": ["nodes"],
- "properties": {"nodes": _ARRAY},
+ "required": ["lxmf_propagation_nodes"],
+ "properties": {"lxmf_propagation_nodes": _ARRAY, "nodes": _ARRAY},
"additionalProperties": True,
}
LXMF_PROPAGATION_STATUS_SCHEMA: dict = {
"type": "object",
- "required": ["status"],
- "properties": {"status": _STRING},
"additionalProperties": True,
}
@@ -432,29 +423,27 @@ MAP_DRAWINGS_SCHEMA: dict = {
MAP_OFFLINE_SCHEMA: dict = {
"type": "object",
- "required": ["tiles"],
- "properties": {"tiles": _ARRAY},
+ "required": ["loaded"],
+ "properties": {"loaded": _BOOLEAN, "tiles": _ARRAY},
"additionalProperties": True,
}
MAP_MBTILES_SCHEMA: dict = {
- "type": "object",
- "required": ["mbtiles"],
- "properties": {"mbtiles": _ARRAY},
- "additionalProperties": True,
+ "type": "array",
+ "items": _OBJECT,
}
TELEMETRY_PEERS_SCHEMA: dict = {
"type": "object",
- "required": ["peers"],
- "properties": {"peers": _ARRAY},
+ "required": ["telemetry"],
+ "properties": {"telemetry": _ARRAY, "peers": _ARRAY},
"additionalProperties": True,
}
TELEMETRY_TRACKING_SCHEMA: dict = {
"type": "object",
- "required": ["tracking"],
- "properties": {"tracking": _ARRAY},
+ "required": ["tracked_peers"],
+ "properties": {"tracked_peers": _ARRAY, "tracking": _ARRAY},
"additionalProperties": True,
}
@@ -474,8 +463,8 @@ TELEMETRY_LATEST_SCHEMA: dict = {
TELEMETRY_HISTORY_SCHEMA: dict = {
"type": "object",
- "required": ["history"],
- "properties": {"history": _ARRAY},
+ "required": ["telemetry"],
+ "properties": {"telemetry": _ARRAY, "history": _ARRAY},
"additionalProperties": True,
}
@@ -516,8 +505,6 @@ RRC_ACTIVITY_SCHEMA: dict = {
RNCP_STATUS_SCHEMA: dict = {
"type": "object",
- "required": ["status"],
- "properties": {"status": _STRING},
"additionalProperties": True,
}
@@ -544,8 +531,6 @@ RNPATH_RATES_SCHEMA: dict = {
RNPATH_TRACE_SCHEMA: dict = {
"type": "object",
- "required": ["trace"],
- "properties": {"trace": _ARRAY},
"additionalProperties": True,
}
@@ -572,8 +557,6 @@ RNSTATUS_SCHEMA: dict = {
BOTS_STATUS_SCHEMA: dict = {
"type": "object",
- "required": ["bots"],
- "properties": {"bots": _ARRAY},
"additionalProperties": True,
}
@@ -586,8 +569,8 @@ BOTS_SUBPROCESS_LOG_SCHEMA: dict = {
SPAM_KEYWORDS_SCHEMA: dict = {
"type": "object",
- "required": ["keywords"],
- "properties": {"keywords": _ARRAY},
+ "required": ["spam_keywords"],
+ "properties": {"spam_keywords": _ARRAY, "keywords": _ARRAY},
"additionalProperties": True,
}
@@ -625,16 +608,17 @@ REPOSITORY_SERVER_STATUS_SCHEMA: dict = {
}
REPOSITORY_SERVER_LIST_SCHEMA: dict = {
- "type": "object",
- "required": ["entries"],
- "properties": {"entries": _ARRAY},
- "additionalProperties": True,
+ "type": "array",
+ "items": _OBJECT,
}
ANNOUNCE_SINGLE_SCHEMA: dict = {
"type": "object",
- "required": ["announce"],
- "properties": {"announce": {"type": ["object", "null"]}},
+ "required": ["message"],
+ "properties": {
+ "message": _STRING,
+ "announce": {"type": ["object", "null"]},
+ },
"additionalProperties": True,
}
@@ -678,15 +662,18 @@ DESTINATION_STAMP_INFO_SCHEMA: dict = {
DESTINATION_SIGNAL_METRICS_SCHEMA: dict = {
"type": "object",
- "required": ["metrics"],
- "properties": {"metrics": {"type": ["object", "null"]}},
+ "required": ["signal_metrics"],
+ "properties": {
+ "signal_metrics": {"type": ["object", "null"]},
+ "metrics": {"type": ["object", "null"]},
+ },
"additionalProperties": True,
}
LXMF_CONVERSATION_MESSAGES_SCHEMA: dict = {
"type": "object",
- "required": ["messages"],
- "properties": {"messages": _ARRAY},
+ "required": ["lxmf_messages"],
+ "properties": {"lxmf_messages": _ARRAY, "messages": _ARRAY},
"additionalProperties": True,
}
@@ -713,15 +700,13 @@ CHANGELOG_SCHEMA: dict = {
TELEPHONE_STATUS_SCHEMA: dict = {
"type": "object",
- "required": ["status"],
- "properties": {"status": _INTEGER},
"additionalProperties": True,
}
TELEPHONE_HISTORY_SCHEMA: dict = {
"type": "object",
- "required": ["history"],
- "properties": {"history": _ARRAY},
+ "required": ["call_history"],
+ "properties": {"call_history": _ARRAY, "history": _ARRAY},
"additionalProperties": True,
}
@@ -756,7 +741,5 @@ TELEPHONE_CODEC2_STATUS_SCHEMA: dict = {
TELEPHONE_CALL_SCHEMA: dict = {
"type": "object",
- "required": ["call"],
- "properties": {"call": {"type": ["object", "null"]}},
"additionalProperties": True,
}
diff --git a/tests/backend/test_http_api_json_contracts_broad.py b/tests/backend/test_http_api_json_contracts_broad.py
index 911077ff..a626aa7a 100644
--- a/tests/backend/test_http_api_json_contracts_broad.py
+++ b/tests/backend/test_http_api_json_contracts_broad.py
@@ -55,13 +55,29 @@ def mock_rns_minimal():
@pytest.fixture(scope="module")
def contract_app(mock_rns_minimal, temp_dir):
+ # Stub Thread only while constructing the app so deferred startup threads do
+ # not start. Leave real Thread available afterward so asyncio.to_thread works
+ # (licenses, memory diag, etc.). Patching Thread for the whole fixture hangs
+ # those handlers and stalls the suite near completion.
with (
patch("meshchatx.meshchat.generate_ssl_certificate"),
patch("psutil.Process") as mock_process,
patch("psutil.net_io_counters") as mock_net_io,
patch("importlib.metadata.version", return_value="1.2.3"),
patch("meshchatx.meshchat.LXST") as mock_lxst,
- patch("threading.Thread"),
+ patch(
+ "meshchatx.src.backend.licenses_collector.build_licenses_payload",
+ return_value={
+ "backend": [{"name": "rns", "version": "1.0", "license": "MIT"}],
+ "frontend": [{"name": "vue", "version": "3.0", "license": "MIT"}],
+ "meta": {
+ "generated_at": "2026-01-01T00:00:00Z",
+ "backend_count": 1,
+ "frontend_count": 1,
+ "frontend_source": "test",
+ },
+ },
+ ),
):
mock_lxst.__version__ = "1.2.3"
mock_proc_instance = mock_process.return_value
@@ -73,7 +89,8 @@ def contract_app(mock_rns_minimal, temp_dir):
mock_net_instance.bytes_recv = 0
mock_net_instance.packets_sent = 0
mock_net_instance.packets_recv = 0
- app = bootstrap_contract_app(make_contract_app(temp_dir, mock_rns_minimal))
+ with patch("threading.Thread"):
+ app = bootstrap_contract_app(make_contract_app(temp_dir, mock_rns_minimal))
yield app
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────